/* Libs being linked in our current Eclipse+MinGW setup:
 opengl32
 glu32
 sfml-graphics-d
 sfml-window-d
 sfml-system-d
 glew32
 */

#include "GL/glew.h"
#include <SFML/Window.hpp>
#include <SFML/Graphics.hpp>
#include <GL/gl.h>
#include <GL/glu.h>
#include <windows.h>
#include <cmath>
#include <iomanip>
#include <fstream>
#include <sstream>
#include <stdexcept>
#include <string>
#include <vector>
#include "camera.h"
#include "mathlib.h"
#include "terrain.h"

const int FPS = 30;
const int NUMBER_OF_TERRAIN_TEXTURES = 5;
const float ROUGHNESS = 1.2f;
const float SCALE = 2.0f;
const float TILING = 12.0f;
const int MAP_SIZE = 128;
const int SPACE_BETWEEN_GRIDS = 16;
const float CAMERA_FOVX = 90.0f;
const float CAMERA_ZFAR = MAP_SIZE * SPACE_BETWEEN_GRIDS * 2.0f;
const float CAMERA_ZNEAR = 1.0f;
const float CAMERA_Y_OFFSET = 25.0f;
const Vector3 CAMERA_ACCELERATION(400.0f, 400.0f, 400.0f);
const Vector3 CAMERA_VELOCITY(100.0f, 100.0f, 100.0f);
const int MAX_REFRESH_COUNT = 255;

struct TerrainRegion {
	float min;
	float max;
	GLuint texture;
	std::string filename;
};

float lastMx;
float lastMy;
int w;
int h;
float lightDirection[4] = { 0.0f, 1.0f, 0.0f, 0.0f };
GLuint shader;
Terrain oldTerrain;
Terrain currentTerrain;
Camera camera;
Vector3 cameraBoundsMax;
Vector3 cameraBoundsMin;
int refreshCount = 0;
float counter = 0;

TerrainRegion regions[NUMBER_OF_TERRAIN_TEXTURES] = { 0.0f, 50.0f * SCALE, 0, "content/textures/tex01.jpg", 51.0f
		* SCALE, 101.0f * SCALE, 0, "content/textures/grass.jpg", 102.0f * SCALE, 203.0f
		* SCALE, 0, "content/textures/rock.jpg", 204.0f * SCALE, 255.0f * SCALE, 0,
		"content/textures/snow.jpg", 0.0f, 0.0f, 0, "content/textures/water.bmp" };

void BindTexture(GLuint texture, GLuint unit);
void Reset();
GLuint CompileShader(GLenum type, const GLchar *pszSource, GLint length);
void GenerateTerrain();
bool Init();
void InitApp();
GLuint CreateShaders(GLuint vertShader, GLuint fragShader);
GLuint LoadShader(std::string &infoLog);
GLuint LoadTexture(const char *pszFilename);
GLuint LoadTexture(const char *pszFilename, GLint magFilter, GLint minFilter, GLint wrapS, GLint wrapT);
void DetectColisions();
void ReadFile(const char *pszFilename, std::string &buffer);
void RenderFrame();
void RenderTerrain();
void UpdateCamera(float elapsedTimeSec, float mouseDx, float mouseDy, Vector3 movementDirection);
void UpdateTerrainShaderParameters();
void ApproximateTerrains();

float lastPos[2]={0.0,0.0};

void Reset() {
	for (int i = 0; i < NUMBER_OF_TERRAIN_TEXTURES; ++i) {
		if (regions[i].texture) {
			glDeleteTextures(1, &regions[i].texture);
			regions[i].texture = 0;
		}
	}

	if (shader) {
		glUseProgram(0);
		glDeleteProgram(shader);
		shader = 0;
	}

	currentTerrain.destroy();
	oldTerrain.destroy();
}

void BindTexture(GLuint texture, GLuint unit) {
	glActiveTexture(GL_TEXTURE0 + unit);
	glEnable(GL_TEXTURE_2D);
	glBindTexture(GL_TEXTURE_2D, texture);
}

GLuint CompileShader(GLenum type, const GLchar *pszSource, GLint length) {
	GLuint shader = glCreateShader(type);

	if (shader) {
		GLint compiled = 0;

		glShaderSource(shader, 1, &pszSource, &length);
		glCompileShader(shader);
		glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);

		if (!compiled) {
			GLsizei infoLogSize = 0;
			std::string infoLog;

			glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &infoLogSize);
			infoLog.resize(infoLogSize);
			glGetShaderInfoLog(shader, infoLogSize, &infoLogSize, &infoLog[0]);

			throw infoLog;
		}
	}

	return shader;
}

void GenerateTerrain() {
	if (!oldTerrain.createMap(ROUGHNESS))
		throw std::runtime_error("Failed to generate terrain.");
	refreshCount = 0;
}

bool Init() {
	try {
		if (!__GLEW_VERSION_2_0)
			throw std::runtime_error("This application requires OpenGL 2.0 support.");

		InitApp();
	} catch (const std::exception &e) {
		std::ostringstream msg;

		msg << "Application initialization failed!" << std::endl << std::endl;
		msg << e.what();

		return false;
	}
	return true;
}

void InitApp() {
	for (int i = 0; i < NUMBER_OF_TERRAIN_TEXTURES; ++i) {
		if (!(regions[i].texture = LoadTexture(regions[i].filename.c_str())))
			throw std::runtime_error("Failed to load texture: " + regions[i].filename);
	}

	std::string infoLog;

	if (!(shader = LoadShader(infoLog)))
		throw std::runtime_error("Failed to load shader: terrain.glsl.\n" + infoLog);

	if (!oldTerrain.create(MAP_SIZE, SPACE_BETWEEN_GRIDS, SCALE)
			|| !currentTerrain.create(MAP_SIZE, SPACE_BETWEEN_GRIDS, SCALE))
		throw std::runtime_error("Failed to create terrain.");

	GenerateTerrain();

	currentTerrain.createMap(ROUGHNESS);

	Vector3 pos;

	pos.x = MAP_SIZE * SPACE_BETWEEN_GRIDS * 0.5f;
	pos.z = MAP_SIZE * SPACE_BETWEEN_GRIDS * 0.5f;
	pos.y = currentTerrain.getHeightMap().heightAt(pos.x, pos.z) + CAMERA_Y_OFFSET;

	camera.setPosition(pos);
	camera.setAcceleration(CAMERA_ACCELERATION);
	camera.setVelocity(CAMERA_VELOCITY);

	camera.perspective(CAMERA_FOVX, static_cast<float>(w) / static_cast<float>(h), CAMERA_ZNEAR,
			CAMERA_ZFAR);

	float upperBounds = (MAP_SIZE * SPACE_BETWEEN_GRIDS - (2.0f * SPACE_BETWEEN_GRIDS));
	float lowerBounds = static_cast<float>(SPACE_BETWEEN_GRIDS);

	cameraBoundsMax.x = upperBounds;
	cameraBoundsMax.y = CAMERA_ZFAR;
	cameraBoundsMax.z = upperBounds;

	cameraBoundsMin.x = lowerBounds;
	cameraBoundsMin.y = 0.0f;
	cameraBoundsMin.z = lowerBounds;

}

GLuint CreateShaders(GLuint vertShader, GLuint fragShader) {
	GLuint program = glCreateProgram();

	if (program) {
		GLint linked = 0;

		if (vertShader)
			glAttachShader(program, vertShader);

		if (fragShader)
			glAttachShader(program, fragShader);

		glLinkProgram(program);
		glGetProgramiv(program, GL_LINK_STATUS, &linked);

		if (!linked) {
			GLsizei infoLogSize = 0;
			std::string infoLog;

			glGetProgramiv(program, GL_INFO_LOG_LENGTH, &infoLogSize);
			infoLog.resize(infoLogSize);
			glGetProgramInfoLog(program, infoLogSize, &infoLogSize, &infoLog[0]);

			throw infoLog;
		}

		if (vertShader)
			glDeleteShader(vertShader);

		if (fragShader)
			glDeleteShader(fragShader);
	}

	return program;
}

GLuint LoadShader(std::string &infoLog) {
	infoLog.clear();

	GLuint program = 0;
	std::string buffer;
	std::string buffer2;

	ReadFile("content/shaders/shader.vert", buffer);
	ReadFile("content/shaders/shader.frag", buffer2);

	if (buffer.length() > 0) {
		const GLchar *pSource = 0;
		GLint length = 0;
		GLuint vertShader = 0;
		GLuint fragShader = 0;

		try {
			pSource = reinterpret_cast<const GLchar *>(&buffer[0]);
			length = static_cast<GLint>(buffer.length());
			vertShader = CompileShader(GL_VERTEX_SHADER, pSource, length);

			pSource = reinterpret_cast<const GLchar *>(&buffer2[0]);
			length = static_cast<GLint>(buffer2.length());
			fragShader = CompileShader(GL_FRAGMENT_SHADER, pSource, length);

			program = CreateShaders(vertShader, fragShader);
		} catch (const std::string &errors) {
			infoLog = errors;
		}
	}

	return program;
}

GLuint LoadTexture(const char *pszFilename) {
	return LoadTexture(pszFilename, GL_LINEAR, GL_LINEAR_MIPMAP_LINEAR, GL_REPEAT, GL_REPEAT);
}

GLuint LoadTexture(const char *pszFilename, GLint magFilter, GLint minFilter, GLint wrapS, GLint wrapT) {
	GLuint id = 0;

	sf::Image texture;


	if (texture.LoadFromFile(pszFilename)) {


		glGenTextures(1, &id);
		glBindTexture(GL_TEXTURE_2D, id);

		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, magFilter);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, minFilter);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrapS);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrapT);
		glTexParameteri(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, GL_TRUE);

		glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, texture.GetHeight(), texture.GetWidth(), 0, GL_RGBA, GL_UNSIGNED_BYTE,
				texture.GetPixelsPtr());

		glBindTexture(GL_TEXTURE_2D, 0);
	}

	return id;
}

void DetectColisions() {
	const Vector3 &pos = camera.getPosition();
	Vector3 newPos(pos);

	if (pos.x > cameraBoundsMax.x)
		newPos.x = cameraBoundsMax.x;

	if (pos.x < cameraBoundsMin.x)
		newPos.x = cameraBoundsMin.x;

	if (pos.z > cameraBoundsMax.z)
		newPos.z = cameraBoundsMax.z;

	if (pos.z < cameraBoundsMin.z)
		newPos.z = cameraBoundsMin.z;

	newPos.y = currentTerrain.getHeightMap().heightAt(newPos.x, newPos.z) + CAMERA_Y_OFFSET;

	camera.setPosition(newPos);
}

void ReadFile(const char *pszFilename, std::string &buffer) {
	std::ifstream file(pszFilename, std::ios::binary);

	if (file.is_open()) {
		file.seekg(0, std::ios::end);

		std::ifstream::pos_type fileSize = file.tellg();

		buffer.resize(static_cast<unsigned int>(fileSize));
		file.seekg(0, std::ios::beg);
		file.read(&buffer[0], fileSize);
	}
}

void RenderFrame() {
	glEnable(GL_DEPTH_TEST);
	glEnable(GL_CULL_FACE);

	glViewport(0, 0, w, h);
	glClearColor(0.4f, 0.6f, 1.0f, 0.0f);
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	glMultMatrixf(&camera.getProjectionMatrix()[0][0]);

	glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();
	glMultMatrixf(&camera.getViewMatrix()[0][0]);

	RenderTerrain();
}

void RenderTerrain() {
	glUseProgram(shader);
	UpdateTerrainShaderParameters();

	glEnable(GL_LIGHTING);
	glEnable(GL_LIGHT0);
	glLightfv(GL_LIGHT0, GL_POSITION, lightDirection);

	BindTexture(regions[0].texture, 0);
	BindTexture(regions[1].texture, 1);
	BindTexture(regions[2].texture, 2);
	BindTexture(regions[3].texture, 3);
	BindTexture(regions[4].texture, 4);

	currentTerrain.draw();

	for (int i = 3; i >= 0; --i) {
		glActiveTexture(GL_TEXTURE0 + i);
		glBindTexture(GL_TEXTURE_2D, 0);
		glDisable(GL_TEXTURE_2D);
	}

	glDisable(GL_LIGHT0);
	glDisable(GL_LIGHTING);

	glUseProgram(0);
}

void UpdateCamera(float elapsedTimeSec, float mouseDx, float mouseDy, Vector3 movementDirection) {
	camera.rotateSmoothly(mouseDx, mouseDy, 0.0f);
	camera.updatePosition(movementDirection, elapsedTimeSec);

	DetectColisions();
}

void UpdateTerrainShaderParameters() {
	GLint location = -1;

	location = glGetUniformLocation(shader, "tilingFactor");
	glUniform1f(location, TILING);

	location = glGetUniformLocation(shader, "region1.max");
	glUniform1f(location, regions[0].max);

	location = glGetUniformLocation(shader, "region1.min");
	glUniform1f(location, regions[0].min);

	location = glGetUniformLocation(shader, "region2.max");
	glUniform1f(location, regions[1].max);

	location = glGetUniformLocation(shader, "region2.min");
	glUniform1f(location, regions[1].min);

	location = glGetUniformLocation(shader, "region3.max");
	glUniform1f(location, regions[2].max);

	location = glGetUniformLocation(shader, "region3.min");
	glUniform1f(location, regions[2].min);

	location = glGetUniformLocation(shader, "region4.max");
	glUniform1f(location, regions[3].max);

	location = glGetUniformLocation(shader, "region4.min");
	glUniform1f(location, regions[3].min);

	glUniform1i(glGetUniformLocation(shader, "region1ColorMap"), 0);
	glUniform1i(glGetUniformLocation(shader, "region2ColorMap"), 1);
	glUniform1i(glGetUniformLocation(shader, "region3ColorMap"), 2);
	glUniform1i(glGetUniformLocation(shader, "region4ColorMap"), 3);
	glUniform1i(glGetUniformLocation(shader, "region5ColorMap"), 4);

	glUniform1f(glGetUniformLocation(shader, "waterLevel"), oldTerrain.getHeightMap().getWaterLevel());
	glUniform1f(glGetUniformLocation(shader, "counter"), counter);
}

void ApproximateTerrains() {
	HeightMap targetMap = oldTerrain.getHeightMap();
	HeightMap currentMap = currentTerrain.getHeightMap();

	int size = targetMap.getSize();
	size *= size;

	for (int i = 0; i < size; i++) {
		float targetHeight = targetMap.heightAtIndex(i);
		float currentHeight = currentMap.heightAtIndex(i);

		float increment = 0;
		if (targetHeight != currentHeight) {
			if (abs(targetHeight - currentHeight) < 2) {
				increment = targetHeight - currentHeight;
			} else {
				// increment = (targetHeight - currentHeight) / 32.0;
				increment = targetHeight > currentHeight ? 1.0f : -1.0f;
			}
		}

		currentTerrain.getHeightMap().SetHeightAtIndex(i, currentHeight + increment);
	}

	currentTerrain.generateVertices();
}

class GLBox {
public:
	float GetElapsedTime() {
		return motionClock2.GetElapsedTime();
	}

	float GetElapsedTimeInSeconds() {
		timeSinceMotion = motionClock.GetElapsedTime();
		motionClock.Reset();
		return timeSinceMotion;
	}

	GLBox() {
		sf::VideoMode Mode = sf::VideoMode::GetDesktopMode();
		w = Mode.Width;
		h = Mode.Height;

		//new SFML window and OpenGL context

		App = new sf::Window(sf::VideoMode(w, h, 32), "Terrain");

		//log file to record any errors that come up
		FILE * logFile;
		logFile = fopen("log.txt", "wb");
		if (logFile == NULL) {
			printf("Unable to open log file. Exiting...\n");
			exit(2);
		}

		__glewInit(logFile);

		if (Init()) {
			App->SetCursorPosition(w / 2, h / 2);
			lastMx = w / 2;
			lastMy = h / 2;

			while (App->IsOpened()) {
				float targetFrameTime = 1.0f / (float) FPS;
				float sleepTime = targetFrameTime - App->GetFrameTime();
				if (sleepTime > 0)
					sf::Sleep(sleepTime);

				App->SetActive();
				App->ShowMouseCursor(false);

				handleEvents();

				counter = GetElapsedTime();
				if (refreshCount < MAX_REFRESH_COUNT) {
					ApproximateTerrains();
				}

				RenderFrame();

				App->Display();
			}
		}

		Reset();
		fclose(logFile);
	}

private:
	sf::Window *App;
	sf::Clock motionClock;
	sf::Clock motionClock2;
	float timeSinceMotion;
	GLint progMaze;
	GLuint textureTarget;

	void handleEvents() {
		const sf::Input& Input = App->GetInput();
		sf::Event Event;

		float mdx, mdy = 0;

		while (App->GetEvent(Event)) {
			// Close window : exit
			if (Event.Type == sf::Event::Closed)
				App->Close();

			// Escape key : exit
			if ((Event.Type == sf::Event::KeyPressed) && (Event.Key.Code == sf::Key::Escape))
				App->Close();

			if ((Event.Type == sf::Event::KeyPressed) && (Event.Key.Code == sf::Key::Space)) {
				GenerateTerrain();
			}

			if (Event.Type == sf::Event::Resized) {
				h = Event.Size.Height;
				w = Event.Size.Width;
				camera.perspective(CAMERA_FOVX, static_cast<float>(w) / static_cast<float>(h),
						CAMERA_ZNEAR, CAMERA_ZFAR);
			}

		}

		int x = Input.GetMouseX();
		int y = Input.GetMouseY();

		mdx = lastMx - x;
		mdy = lastMy - y;

		App->SetCursorPosition(w / 2, h / 2);

		lastMx = x + mdx;
		lastMy = y + mdy;

		static bool moveForwardsPressed = false;
		static bool moveBackwardsPressed = false;
		static bool moveRightPressed = false;
		static bool moveLeftPressed = false;
		static bool moveUpPressed = false;
		static bool moveDownPressed = false;

		Vector3 direction(0.0f, 0.0f, 0.0f);
		Vector3 velocity = camera.getCurrentVelocity();

		if (Input.IsKeyDown(sf::Key::Up) || Input.IsKeyDown(sf::Key::W)) {
			if (!moveForwardsPressed) {
				moveForwardsPressed = true;
				camera.setCurrentVelocity(velocity.x, velocity.y, 0.0f);
			}

			direction.z += 1.0f;
		} else {
			moveForwardsPressed = false;
		}

		if (Input.IsKeyDown(sf::Key::Down) || Input.IsKeyDown(sf::Key::S)) {
			if (!moveBackwardsPressed) {
				moveBackwardsPressed = true;
				camera.setCurrentVelocity(velocity.x, velocity.y, 0.0f);
			}

			direction.z -= 1.0f;
		} else {
			moveBackwardsPressed = false;
		}

		if (Input.IsKeyDown(sf::Key::Right) || Input.IsKeyDown(sf::Key::D)) {
			if (!moveRightPressed) {
				moveRightPressed = true;
				camera.setCurrentVelocity(0.0f, velocity.y, velocity.z);
			}

			direction.x += 1.0f;
		} else {
			moveRightPressed = false;
		}

		if (Input.IsKeyDown(sf::Key::Left) || Input.IsKeyDown(sf::Key::A)) {
			if (!moveLeftPressed) {
				moveLeftPressed = true;
				camera.setCurrentVelocity(0.0f, velocity.y, velocity.z);
			}

			direction.x -= 1.0f;
		} else {
			moveLeftPressed = false;
		}

		UpdateCamera(GetElapsedTimeInSeconds(), mdx / 5.0, mdy / 5.0, direction);
	}

	void __glewInit(FILE * logFile = NULL) const {
		GLenum err = glewInit();
		if (GLEW_OK != err) {
			//Problem: glewInit failed, something is seriously wrong.
			fprintf(stderr, "Error: %s\n", glewGetErrorString(err));
			if(logFile!=NULL) fprintf(logFile, "Error: %s\n",glewGetErrorString(err));
		}
		else
		{
			printf("GLEW init finished...\n");
			if(logFile!=NULL) fprintf(logFile, "GLEW init finished...\n");
			if( __GLEW_VERSION_2_0 )
			{
				printf("OpenGL 2.0 is supported. Shaders should run correctly.\n");
				if(logFile!=NULL) fprintf(logFile, "OpenGL 2.0 is supported. Shaders should run correctly.\n");
			}
			else
			{
				printf("OpenGL 2.0 is NOT enabled. The program may not work correctly.\n");
				if(logFile!=NULL) fprintf(logFile,"OpenGL 2.0 is NOT enabled. The program may not work correctly.\n");
			}

			if( GLEW_ARB_vertex_program )
			{
					printf("ARB vertex programs supported.\n");
					if(logFile!=NULL) fprintf(logFile, "ARB vertex programs supported.\n");
			}
			else
			{
					printf("ARB vertex programs NOT supported. The program may not work correctly.\n");
					if(logFile!=NULL) fprintf(logFile,"ARB vertex programs NOT supported. The program may not work correctly.\n");
			}
			if( GLEW_ARB_fragment_program )
			{
					printf("ARB fragment programs supported.\n");
					if(logFile!=NULL) fprintf(logFile, "ARB fragment programs supported.\n");
			}
			else
			{
					printf("ARB fragment programs NOT supported. The program may not work correctly.\n");
					if(logFile!=NULL) fprintf(logFile, "ARB fragment programs NOT supported. The program may not work correctly.\n");
			}
		}
		}
	};

int main(int argc, char **argv) {
#ifdef __APPLE__
#define pathSize 5000
	char path[pathSize];
	uint32_t size = pathSize;
	_NSGetExecutablePath(path, &size);
	char *slashPos = strrchr(path, '/');
	slashPos[0] = '\0';
	chdir(path);
	chdir("../../../");
#endif

	GLBox prog;
	return EXIT_SUCCESS;

}
